/*
 * Copyright 1999-2018 Alibaba Group Holding Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.alibaba.nacos.core.listener;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.io.Resource;

import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.exception.runtime.NacosRuntimeException;
import com.alibaba.nacos.common.event.ServerConfigChangeEvent;
import com.alibaba.nacos.common.executor.ExecutorFactory;
import com.alibaba.nacos.common.executor.NameThreadFactory;
import com.alibaba.nacos.common.executor.ThreadPoolManager;
import com.alibaba.nacos.common.notify.NotifyCenter;
import com.alibaba.nacos.common.utils.StringUtils;
import com.alibaba.nacos.sys.env.EnvUtil;
import com.alibaba.nacos.sys.file.FileChangeEvent;
import com.alibaba.nacos.sys.file.FileWatcher;
import com.alibaba.nacos.sys.file.WatchFileCenter;
import com.alibaba.nacos.sys.utils.ApplicationUtils;
import com.alibaba.nacos.sys.utils.DiskUtils;
import com.alibaba.nacos.sys.utils.InetUtils;

/**
 * init environment config.
 *
 * @author <a href="mailto:huangxiaoyu1018@gmail.com">hxy1991</a>
 * @since 0.5.0
 */
public class StartingApplicationListener implements NacosApplicationListener {

	private static final Logger LOGGER = LoggerFactory.getLogger(StartingApplicationListener.class);

	private static final String MODE_PROPERTY_KEY_STAND_MODE = "nacos.mode";

	private static final String MODE_PROPERTY_KEY_FUNCTION_MODE = "nacos.function.mode";

	private static final String LOCAL_IP_PROPERTY_KEY = "nacos.local.ip";

	private static final String NACOS_APPLICATION_CONF = "nacos_application_conf";

	private static final String NACOS_MODE_STAND_ALONE = "standalone";

	private static final String NACOS_MODE_CLUSTER = "cluster";

	private static final String DEFAULT_FUNCTION_MODE = "All";

	// private static final String DEFAULT_DATABASE = "mysql";

	private static final String DEFAULT_DATABASE = "oracle";

	private static final String DATASOURCE_PLATFORM_PROPERTY = "spring.datasource.platform";

	private static final String DEFAULT_DATASOURCE_PLATFORM = "";

	private static final String DATASOURCE_MODE_EXTERNAL = "external";

	private static final String DATASOURCE_MODE_EMBEDDED = "embedded";

	private static final String EMBEDDED_STORAGE = "embeddedStorage";

	private static final Map<String, Object> SOURCES = new ConcurrentHashMap<>();

	private ScheduledExecutorService scheduledExecutorService;

	private volatile boolean starting;

	@Override
	public void starting() {
		starting = true;
	}

	@Override
	public void environmentPrepared(ConfigurableEnvironment environment) {
		makeWorkDir();

		injectEnvironment(environment);

		loadPreProperties(environment);

		initSystemProperty();
	}

	@Override
	public void contextPrepared(ConfigurableApplicationContext context) {
		logClusterConf();

		logStarting();
	}

	@Override
	public void contextLoaded(ConfigurableApplicationContext context) {

	}

	@Override
	public void started(ConfigurableApplicationContext context) {
		starting = false;

		closeExecutor();

		ApplicationUtils.setStarted(true);
		judgeStorageMode(context.getEnvironment());
	}

	@Override
	public void running(ConfigurableApplicationContext context) {
	}

	@Override
	public void failed(ConfigurableApplicationContext context, Throwable exception) {
		starting = false;

		makeWorkDir();

		LOGGER.error("Startup errors : ", exception);
		ThreadPoolManager.shutdown();
		WatchFileCenter.shutdown();
		NotifyCenter.shutdown();

		closeExecutor();

		context.close();

		LOGGER.error("Nacos failed to start, please see {} for more details.", Paths.get(EnvUtil.getNacosHome(), "logs/nacos.log"));
	}

	private void injectEnvironment(ConfigurableEnvironment environment) {
		EnvUtil.setEnvironment(environment);
	}

	private void loadPreProperties(ConfigurableEnvironment environment) {
		try {
			SOURCES.putAll(EnvUtil.loadProperties(EnvUtil.getApplicationConfFileResource()));
			SOURCES.put("db.password.0", encryptDbFile());
			environment.getPropertySources().addLast(new OriginTrackedMapPropertySource(NACOS_APPLICATION_CONF, SOURCES));
			registerWatcher();
		} catch (Exception e) {
			throw new NacosRuntimeException(NacosException.SERVER_ERROR, e);
		}
	}

	/**
	 * 文件中的初始密码为明文,读取到明文后进行加密并替换原有密码
	 * 
	 * SEC{加密后的密码}
	 * 
	 */
	private String encryptDbFile() {
		try {
			Resource dbFileResource = EnvUtil.getDBConfFileResource();
			File dbfile = dbFileResource.getFile();

			String dbPassword = StringUtils.trim(FileUtils.readFileToString(dbfile, StandardCharsets.UTF_8));
			if (StringUtils.isBlank(dbPassword)) {
				throw new NacosRuntimeException(999, "数据库密码不能为空,检查db.sec文件是否正确");
			}

			if (dbPassword.startsWith("SEC{")) {
				//密码已经被加密,开始解密
				Matcher matcher = Pattern.compile("(?<=SEC\\{).*(?=})").matcher(dbPassword);
				String encryptPasswordString = "";
				while (matcher.find()) {
					encryptPasswordString = matcher.group(0);
					break;
				}
				if (StringUtils.isBlank(encryptPasswordString)) {
					throw new NacosRuntimeException(999, "数据库密码格式不对,必须是SEC{XXX}");
				}
				String decodePasswordString = DESUtils.decode(encryptPasswordString);
				LOGGER.info("encryptDbFile decode OK");

				return decodePasswordString;
			} else {
				//未加密
				String encryptPasswordString = "SEC{" + DESUtils.encode(dbPassword) + "}";
				FileUtils.writeStringToFile(dbfile, encryptPasswordString, StandardCharsets.UTF_8);

				LOGGER.info("encryptDbFile encode OK");
				return dbPassword;
			}
			/*System.out.println(dbFileResource);
			System.out.println(dbFileResource.getFile());
			System.out.println(dbFileResource.getURI());
			System.out.println(dbFileResource.getURL());
			System.out.println(dbFileResource.getInputStream());*/
			/*class path resource [db.sec]
			D:\soft\git\nacos-2.0.4\console\target\classes\db.sec
			file:/D:/soft/git/nacos-2.0.4/console/target/classes/db.sec
			file:/D:/soft/git/nacos-2.0.4/console/target/classes/db.sec*/

		} catch (IOException e) {
			LOGGER.info("加密nacos数据库文件失败");
			throw new NacosRuntimeException(999, e);
		}

	}

	private void registerWatcher() throws NacosException {

		WatchFileCenter.registerWatcher(EnvUtil.getConfPath(), new FileWatcher() {
			@Override
			public void onChange(FileChangeEvent event) {
				try {
					Map<String, ?> tmp = EnvUtil.loadProperties(EnvUtil.getApplicationConfFileResource());
					SOURCES.putAll(tmp);
					NotifyCenter.publishEvent(ServerConfigChangeEvent.newEvent());
				} catch (IOException ignore) {
					LOGGER.warn("Failed to monitor file ", ignore);
				}
			}

			@Override
			public boolean interest(String context) {
				return StringUtils.contains(context, "application.properties");
			}
		});

	}

	private void initSystemProperty() {
		if (EnvUtil.getStandaloneMode()) {
			System.setProperty(MODE_PROPERTY_KEY_STAND_MODE, NACOS_MODE_STAND_ALONE);
		} else {
			System.setProperty(MODE_PROPERTY_KEY_STAND_MODE, NACOS_MODE_CLUSTER);
		}
		if (EnvUtil.getFunctionMode() == null) {
			System.setProperty(MODE_PROPERTY_KEY_FUNCTION_MODE, DEFAULT_FUNCTION_MODE);
		} else if (EnvUtil.FUNCTION_MODE_CONFIG.equals(EnvUtil.getFunctionMode())) {
			System.setProperty(MODE_PROPERTY_KEY_FUNCTION_MODE, EnvUtil.FUNCTION_MODE_CONFIG);
		} else if (EnvUtil.FUNCTION_MODE_NAMING.equals(EnvUtil.getFunctionMode())) {
			System.setProperty(MODE_PROPERTY_KEY_FUNCTION_MODE, EnvUtil.FUNCTION_MODE_NAMING);
		}

		System.setProperty(LOCAL_IP_PROPERTY_KEY, InetUtils.getSelfIP());
	}

	private void logClusterConf() {
		if (!EnvUtil.getStandaloneMode()) {
			try {
				List<String> clusterConf = EnvUtil.readClusterConf();
				LOGGER.info("The server IP list of Nacos is {}", clusterConf);
			} catch (IOException e) {
				LOGGER.error("read cluster conf fail", e);
			}
		}
	}

	private void closeExecutor() {
		if (scheduledExecutorService != null) {
			scheduledExecutorService.shutdownNow();
		}
	}

	private void makeWorkDir() {
		String[] dirNames = new String[] { "logs", "conf", "data" };
		for (String dirName : dirNames) {
			LOGGER.info("Nacos Log files: {}", Paths.get(EnvUtil.getNacosHome(), dirName));
			try {
				DiskUtils.forceMkdir(new File(Paths.get(EnvUtil.getNacosHome(), dirName).toUri()));
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
	}

	private void logStarting() {
		if (!EnvUtil.getStandaloneMode()) {

			scheduledExecutorService = ExecutorFactory.newSingleScheduledExecutorService(new NameThreadFactory("com.alibaba.nacos.core.nacos-starting"));

			scheduledExecutorService.scheduleWithFixedDelay(() -> {
				if (starting) {
					LOGGER.info("Nacos is starting...");
				}
			}, 1, 1, TimeUnit.SECONDS);
		}
	}

	private void judgeStorageMode(ConfigurableEnvironment env) {

		// External data sources are used by default in cluster mode

		//boolean useExternalStorage = (DEFAULT_DATABASE.equalsIgnoreCase(env.getProperty(DATASOURCE_PLATFORM_PROPERTY, DEFAULT_DATASOURCE_PLATFORM)));

		boolean useExternalStorage = !Boolean.getBoolean(EMBEDDED_STORAGE);

		// must initialize after setUseExternalDB
		// This value is true in stand-alone mode and false in cluster mode
		// If this value is set to true in cluster mode, nacos's distributed
		// storage engine is turned on
		// default value is depend on ${nacos.standalone}

		/*if (!useExternalStorage) {
			boolean embeddedStorage = EnvUtil.getStandaloneMode() || Boolean.getBoolean("embeddedStorage");
			// If the embedded data source storage is not turned on, it is
			// automatically
			// upgraded to the external data source storage, as before
			if (!embeddedStorage) {
				useExternalStorage = true;
			}
		}*/

		LOGGER.info("Nacos started successfully in {} mode. use {} storage", System.getProperty(MODE_PROPERTY_KEY_STAND_MODE), useExternalStorage ? DATASOURCE_MODE_EXTERNAL : DATASOURCE_MODE_EMBEDDED);
	}
}
